#!python
# Peer Data Client
from .encoding import *
from .runtime import *
from .network import *
from .security import *
from .packaging import *
from .application import *
from .architecture import *

import pdb
import socket
from contextlib import closing as Session

def DEBUG(*args):
    pass # print ' '.join(map(str, args))

class APICall:
    def __init__(self, call, getprop = None):
        self.call = call
        self.getprop = getprop
    def __call__(self, name):
        return self.APIMethod(name, self.call, self.getprop)

    def __repr__(self):
        return '<%s>' % (self.__class__.__name__)

    class APIHandle:
        class APIHandleCall:
            def __init__(self, call, getprop = None):
                self.__call = call
                self.__getprop = getprop

            def __getattr__(self, name):
                return Method(name, self.__call, self.__getprop)

            def __repr__(self):
                return '<%s: %r>' % (self.__class__.__name__, self.__api)

        def __init__(self, api, name):
            self.api = api
            self.name = name
            # Q: Why not 'call'? to match Client<-MethodCall
            self.invoke = self.APIHandleCall(api.call, api.getprop)
            self.closed = False

        def __repr__(self):
            return '<%s: %s>' % (self.__class__.__name__, self.name)

        def __del__(self):
            self.close()
        def close(self):
            if not self.closed:
                self.api.call('closeApi')
                self.closed = True

    def open(self, name):
        self.call('openApi', name)
        return self.APIHandle(self, name)

    class APIMethod(MethodCall):
        def __init__(self, name, call, getprop = None):
            self.__name = name
            self.__call = call
            self.__getprop = getprop

        def __repr__(self):
            return '<%s: %s>' % (self.__class__.__name__, self.__name)

        def __getattr__(self, name):
            return Method(name, self.__call, self.__getprop)

        # Context Control:
        def __enter__(self):
            self.__call('openApi', self.__name)
            return self

        def __exit__(self, etype = None, value = None, tb = None):
            self.__call('closeApi')

debug = False
def setDebug(state):
    global debug
    debug = state

class AsyncoreControl:
    TIMEOUT = 0.3 # Good responsiveness
    _asyncore_run_lock = threading.Lock()

    @classmethod
    def runAsyncore(self):
        with self._asyncore_run_lock:
            # XXX this needs to be atomic with the lock, but I don't want
            # to have to lock for every socket map check.  So, try not to
            # start a new connection immediately after the last one closes.
            # (I could probably use some other threading object)
            while asyncore.socket_map:
                ##    if debug:
                ##        import pdb; pdb.set_trace()

                asyncore.loop(timeout = self.TIMEOUT,
                              count = 1) # , use_poll = True)

    @classmethod
    def startAsyncore(self):
        if self._asyncore_run_lock.acquire(False):
            nth(self.runAsyncore)
            self._asyncore_run_lock.release()

class Client(PackageReader, asyncore.dispatcher_with_send, AsyncoreControl):
    @classmethod
    def Open(self, address, port, wait = True):
        handler = self()
        handler.openConnection(address, port)
        if wait:
            handler.WaitForConnection()

        return handler

    def __init__(self, *args, **kwd):
        asyncore.dispatcher_with_send.__init__(self, *args, **kwd)
        PackageReader.__init__(self)
        self.dataChannel = EntitySpace()
        self.deferred_responses = {} # Ordered?
        self.command_nr = 0

        deferredMutex = synchronizedWith(None, recursive = True)
        self.pushResponse = deferredMutex(self.pushResponse)
        self.getWaitState = deferredMutex(self.getWaitState)

        self.call = MethodCall(self.callCommand, self.getEntityProperty)
        self.api = APICall(self.callCommand, self.getEntityProperty)

    def openConnection(self, address, port):
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connectWait = threading.Event()
        self.connectError = None
        self.connect((address, port))

        self.startAsyncore()

    def handle_connect(self):
        self.connectWait.set()

    def handle_read(self):
        try: PackageReader.handle_read(self)
        except socket.error, e:
            self.connectError = e
            self.connectWait.set()

    def handle_write_event(self):
        try: asyncore.dispatcher_with_send.handle_write_event(self)
        except socket.error, e:
            self.connectError = e
            self.connectWait.set()

    def handle_expt_event(self):
        # Strangely, this is called after a failed write for refused connections.
        # So we're racing on connectError (and double connectWait.set, btw)
        try: asyncore.dispatcher_with_send.handle_expt_event(self)
        except socket.error, e:
            self.connectError = e
            self.connectWait.set()

    def WaitForConnection(self):
        while not self.connectWait.isSet():
            self.connectWait.wait()

        if self.connected:
            return True

        if self.connectError:
            raise self.connectError

    def __repr__(self):
        cls = self.__class__
        if self.addr:
            return '<%s.%s: [%s:%d]>' % (cls.__module__, cls.__name__) + self.addr

        return '<%s.%s>' % (cls.__module__, cls.__name__)

    def sendSerialCommand(self, command, (serialId, flags), *args, **kwd):
        data = self.dataChannel.encodeSerialCommand(command, (serialId, flags), *args, **kwd)
        DEBUG('COMMAND[#%d]: %s' % (serialId, data))
        self.send(data)

    def newSerialId(self):
        try: return self.command_nr
        finally: self.command_nr += 1

    def callCommand(self, command, *args, **kwd):
        serialId = self.newSerialId()
        self.sendSerialCommand(command, (serialId, 0), *args, **kwd)
        response = self.waitForResponse(serialId)
        return interpretResponse(response,
                                 callEntityMethod = self.callEntityMethod,
                                 getEntityProperty = self.getEntityProperty)

    rpc_callEntityMethod = 'callEntityMethod'
    def callEntityMethod(self, proxy, *args, **kwd):
        return self.callCommand(self.rpc_callEntityMethod, proxy.refId, *args, **kwd)

    rpc_getEntityProperty = 'getEntityProperty'
    def getEntityProperty(self, proxy, name):
        return self.callCommand(self.rpc_getEntityProperty, proxy.refId, name)

    def handleIncomingPackage(self, package):
        DEBUG('RECEIVED-PACKAGE:', package)
        response = Response.FromPackage(package, self.dataChannel)
        if response is not None:
            serialId = response.serialId
            if serialId is None:
                # Do some default handling.
                try: print interpretResponse(response)
                except Fault, fault:
                    print fault
            else:
                DEBUG('PUSHING RESPONSE:', response)
                self.pushResponse(serialId, response)

    def pushResponse(self, serialId, response):
        # Notify waiter.
        try: waiting = self.deferred_responses[serialId]
        except KeyError:
            DEBUG('GOT RESPONSE BEFORE WAITER: [#%d]' % serialId)
            waiting = [None, response]
            self.deferred_responses[serialId] = waiting
        else:
            DEBUG('POSTING RESPONSE TO WAITER: [#%d]' % serialId)
            del self.deferred_responses[serialId]
            waiting[1] = response
            waiting[0].set()

    def getWaitState(self, serialId):
        try: waiting = self.deferred_responses[serialId]
        except KeyError:
            waiting = [threading.Event(), None]
            self.deferred_responses[serialId] = waiting

        return waiting

    def waitForResponse(self, serialId, timeout = 0.4):
        waiting = self.getWaitState(serialId)
        if waiting[0] is not None:
            wait = waiting[0]
            DEBUG('WAITING FOR [#%d]...' % serialId)
            while not wait.isSet():
                wait.wait(timeout = timeout)

        return waiting[1]

    ##    def write_data(self, data):
    ##        print '...', data
    ##        return PackageReader.write_data(self, data)

class Authorization:
    def __init__(self, username, secretKey):
        self.username = username
        self.secretKey = secretKey

    def Open(self, hostname, port):
        client = Client.Open(hostname, port)
        return self.Authenticate(client)

    def Authenticate(self, client):
        auth = CalculateDigest(self.secretKey, self.username)
        client.call.login(self.username, auth)
        return client

def printFault(fault, cause = False):
    if cause:
        print 'Fault Caused Locally By:'
        printException()
        print

    print 'REMOTE FAULT:'
    print fault.toString(tb = True)

class CompressIfWorthIt:
    THRESHOLD = .70
    All = Object()

    def __init__(self, threshold = None, level = None):
        self.threshold = self.THRESHOLD
        self.level = level if level is not None else self.All

    def __iter__(self):
        if self.level is self.All:
            yield 3
            # yield 2
            yield 1
        else:
            yield self.level

    def worthIt(self, z, u):
        print 'WORTH IT??: (%d/%d) %f' % (z, u, float(z) / u)

        # This probably will only be worth it for messages of a certain size.
        return (float(z) / u) < self.threshold

    def __call__(self, data):
        data_length = len(data)
        for level in self:
            compressed = Compress(data, level)
            if self.worthIt(len(compressed), data_length):
                return compressed

        return data

# Front End.
DEFAULT_PORT = Application.DEFAULT_PORT
DEFAULT_HOST = 'localhost'

def ApplyCompressionChannel(channel, level = None, threshold = None):
    if level is not None:
        if isinstance(level, basestring):
            if level.isdigit():
                level = int(level)
            else:
                assert level == 'all'

        if isinstance(threshold, basestring):
            import re
            m = re.match('^(\d+)%$', threshold)
            assert m is not None
            threshold = int(m.groups()[0]) / 100.0

        channel.compression_level = CompressIfWorthIt(threshold, level)

class ClientOptions(Component):
    @classmethod
    def addCmdlnOptions(self, parser):
        parser.add_option('--port', default = DEFAULT_PORT, type = int)
        parser.add_option('--service-partner-name')
        parser.add_option('--address', default = DEFAULT_HOST)
        parser.add_option('--username')
        parser.add_option('--secret-key', default = '')
        parser.add_option('--api-test', action = 'store_true')
        parser.add_option('--quit', action = 'store_true')
        parser.add_option('--examine', action = 'store_true')
        parser.add_option('--compression-level')
        parser.add_option('--compression-threshold')
        parser.add_option('--open-system-api', action = 'store_true')
        parser.add_option('--enable-timing-tests', action = 'store_true')
        parser.add_option('--open-spline-api', action = 'store_true')
        parser.add_option('-g', '--debug', action = 'store_true')
        parser.add_option('--open-api')

    @classmethod
    def getCmdlnParser(self):
        from optparse import OptionParser
        parser = OptionParser()
        self.addCmdlnOptions(parser)
        return parser

    @classmethod
    def parseCmdln(self, argv = None):
        parser = self.getCmdlnParser()
        return parser.parse_args(argv)

def testApi(client):
    with client.api('API::Management') as api:
        print 'Loading ClockService...'
        print api.loadNewServiceApi('pentacle.application.ClockService').NAME

        print 'Loading SystemDebug...'
        print api.loadNewServiceApi('pentacle.application.SystemDebug').NAME

        print 'Loading ObjectDirectory...'
        print api.loadNewServiceApi('pentacle.application.ObjectDirectory').NAME

        print 'Loading AuxiliaryApi...'
        print api.loadNewServiceApi('pentacle.storage.StorageManagement.AuxiliaryApi').NAME

    print 'Start Ticking...'
    with client.api('ClockService::API') as clock:
        clock.setClockTicking(10000)
        print 'Current Clock Time:', clock.getClockTime()

    print 'Testing Debug...'
    # client.debug = 1
    with client.api('System::Debugging') as debug:
        try: debug.fail()
        except Fault, fault:
            printFault(fault)

    print 'Testing Object Directory...'
    with client.api('System::Directory') as directory:
        N = directory.newObject
        directory.setObject('Feature', N(Aspect = N(Scope = N(Name = 'A System Feature Aspect Scope'))))

        print directory.getObject('Feature').Aspect.Scope.Name

    print 'Testing Auxiliary Storage'
    with client.api('Storage::Aux') as aux:
        db = aux.buildAuxiliaryStore(buildCmdln(db_path = '~/.pentacle/application.db'))
        user = aux.lookupUnitAccess('UserStorage')
        fraun = user.Open(db.application, 'fraun')

        # change user-fraun
        print dict(fraun.unit.unit.items())

def setupClient(client, options):
    import pentacle
    import sys

    ns = dict(peer = client, client = client,
              call = client.call, options = options,
              pentacle = pentacle, sys = sys,
              g = Synthetic(**globals()))

    if options.open_system_api:
        system = client.api.open('System::Debugging')
        e = system.invoke.evaluate

        ns['system'] = system
        ns['e'] = e = system.invoke.evaluate
        ns['x'] = x = system.invoke.execute

        if options.enable_timing_tests:
            def testTiming(n = 2017):
                return len(e('"A" * %d' % n))

            def timeIt(n, number, repeat = 1):
                from timeit import repeat as tFunc
                return tFunc(lambda:testTiming(n),
                             number = number,
                             repeat = repeat)

            ns['tt'] = testTiming
            ns['ti'] = timeIt

    elif options.open_spline_api:
        def PrintUptime(svcmgr):
            print 'Uptime: %(running_duration)s' % svcmgr.invoke.GetManagerStats()

        ns['spline'] = client.api.open('ServiceManager::Spline')
        ns['PrintUptime'] = PrintUptime

    elif options.open_api:
        (variable, name) = options.open_api.split(':', 1)
        ns[variable] = client.api.open(name)

    return ns

def optionalArgs(options, *names):
    kwd = {}
    for n in names:
        value = getattr(options, n, None)
        if value is not None:
            kwd[n] = value

    return kwd

def main(argv = None):
    (options, args) = ClientOptions.parseCmdln(argv)

    # Start connection.
    if options.service_partner_name:
        try:
            from pentacle.bus.partners import OpenPartneredClient
            client = OpenPartneredClient(options.service_partner_name,
                                         **optionalArgs(options, 'username', 'port'))
        except Fault, fault:
            print 'SERVICE-MANAGER FAULT:\n%s' % fault.toString(True)
            return
    else:
        print 'Connecting to [%s:%d]...' % (options.address, options.port)
        client = Client.Open(options.address, options.port)

    ApplyCompressionChannel(client.dataChannel,
                            options.compression_level,
                            options.compression_threshold)

    if options.debug:
        pdb.set_trace()

    # Handle Session.
    username = options.username
    if username:
        try:
            print 'Logging in %s...' % username
            auth = Authorization(username, options.secret_key)
            auth.Authenticate(client)

            if options.api_test:
                testApi(client)

            if options.examine:
                try: import readline
                except ImportError:
                    pass

                ns = setupClient(client, options)

                import pentacle, sys
                from code import InteractiveConsole as IC
                ic = IC(locals = ns)
                ic.interact()

            if options.quit:
                print 'Quitting Application...'
                client.call.stopApplication()

        except Fault, fault:
            printFault(fault, cause = True)
        except KeyboardInterrupt:
            print 'Console Break'

if __name__ == '__main__':
    main()
